﻿using System;
using System.Text;
using System.Collections.Generic;
using OpenTK;

using MosCraft.Utilities;
using MosCraft.Graphics;
using GLSL;

namespace MosCraft
{
    public class Chunk : IDisposable
    {
        /// <summary>
        /// Chunk Size Constants
        /// </summary>
        public const int _XDIM = 16;
        public const int _YDIM = 128;
        public const int _ZDIM = 16;

        public int xPosition { get; private set; }
        public int zPosition { get; private set; }
        public long lastSaveTime;
        public bool isTerrainPopulated;
        public bool isChunkLoaded;
        public bool isModified;
        public bool isVisible;
        public bool isDirty;
        public readonly AxisAlignedBB chunkBoundingBox;
        public ChunkState State;

        public byte[] blocks;
        private byte[] heightMap;
        private World world;
        private NibbleArray metaData;

        #region Rendering properties
        public Vertex[] solidVertices;
        public Vertex[] translucentVertices;
        public VBO solidVBOHandler;
        public VBO translucentVBOHandler;
        #endregion

        public Chunk(World world, int x, int z)
        {
            this.xPosition = x;
            this.zPosition = z;
            this.world = world;
            this.heightMap = new byte[256];
            this.lastSaveTime = 0L;

            //Used for Fustrum detection
            int offsetX = (x << 4);
            int offsetZ = (z << 4);
            this.chunkBoundingBox = AxisAlignedBB.getBoundingBox(offsetX, 0.0D, offsetZ, (offsetX + _XDIM), _YDIM, (offsetZ + _ZDIM));

            if (MosCraft.DEBUG)
            {
                Console.WriteLine("Chunk Created at x:" + x + " z:" + z);
            }
        }

        public Chunk(World world, byte[] rawData, int x, int z)
            : this(world, x, z)
        {
            this.setData(rawData);
        }

        ~Chunk()
        {
            this.Dispose();
        }

        public void Dispose()
        {
            this.blocks = null;
            this.metaData = null;
            this.solidVertices = null;
            this.translucentVertices = null;
            this.solidVBOHandler = null;
            this.translucentVertices = null;
            this.onChunkUnload();
            GC.SuppressFinalize(this);
        }

        public bool isAtLocation(int x, int z)
        {
            return x == xPosition && z == zPosition;
        }

        public void onChunkLoad()
        {
            this.isChunkLoaded = true;
        }

        public void onChunkUnload()
        {
            this.isChunkLoaded = false;
        }

        public void setChunkModified()
        {
            this.isModified = true;
        }

        public bool isEmpty()
        {
            return false;
        }

        #region Chunk data

        public void setData(byte[] rawData)
        {
            this.blocks = rawData;
            this.metaData = new NibbleArray(rawData.Length);
            this.generateHeightMap();
        }

        private void offSet(ref int x, ref int y, ref int z)
        {
            x = (x % _XDIM + _XDIM) % _XDIM;
            y = (y % _YDIM + _YDIM) % _YDIM;
            z = (z % _ZDIM + _ZDIM) % _ZDIM;
        }

        public int getBlockID(int x, int y, int z)
        {
            this.offSet(ref x, ref y, ref z);

            try
            {
                return blocks[x << 11 | z << 7 | y] & 0xff;
            }
            catch // Ugly Fix
            {
                return 0;
            }    
        }

        public int getBlockMetadata(int x, int y, int z)
        {
            this.offSet(ref x, ref y, ref z);
            return this.metaData.getNibble(x, y, z);
        }

        public void setBlockMetadata(int x, int y, int z, int data)
        {
            this.isModified = true;
            this.offSet(ref x, ref y, ref z);
            this.metaData.setNibble(x, y, z, (byte)data);
        }

        public bool setBlockID(int x, int y, int z, int blockID)
        {
            byte block = (byte)blockID;
            this.offSet(ref x, ref y, ref z);

            int height = heightMap[z << 4 | x] & 0xff;
            int existingBlock = blocks[x << 11 | z << 7 | y] & 0xff;

            if (existingBlock == blockID)
            {
                return false;
            }

            blocks[x << 11 | z << 7 | y] = (byte)(blockID & 0xff);
            this.isModified = true;
            this.State = ChunkState.AwaitingRebuild;
            return true;          
        }

        public bool setBlockIDWithMetadata(int x, int y, int z, int blockID, int data)
        {
            byte block = (byte)blockID;
            this.offSet(ref x, ref y, ref z);
            int height = heightMap[z << 4 | x] & 0xff;
            int existingBlock = blocks[x << 11 | z << 7 | y] & 0xff;

            if (existingBlock == blockID && metaData.getNibble(x, y, z) == data)
            {
                return false;
            }

            blocks[x << 11 | z << 7 | y] = (byte)(block & 0xff);
            this.metaData.setNibble(x, y, z, (byte)data);
            this.isModified = true;
            this.State = ChunkState.AwaitingRebuild;
            return true;
        }

        public int getHeightValue(int x, int z)
        {
            int y = 0;
            this.offSet(ref x, ref y, ref z);
            return heightMap[x << 4 | z] & 0xff;
        }

        public bool canBlockSeeTheSky(int x, int y, int z)
        {
            this.offSet(ref x, ref y, ref z);
            return y >= (heightMap[z << 4 | x] & 0xff);
        }

        public void generateHeightMap()
        {
            int y = 127;
            for (int x = 0; x < 16; x++)
            {
                for (int z = 0; z < 16; z++)
                {
                    int l = 127;
                    for (int i1 = x << 11 | z << 7; l > 0 && Block.lightOpacity[blocks[(i1 + l) - 1] & 0xff] == 0; l--) { }
                    heightMap[z << 4 | x] = (byte)l;
                    if (l < y)
                    {
                        y = l;
                    }
                }
            }

            //lowestBlockHeight = i;
            isModified = true;
        }
        #endregion

        public override string ToString()
        {
            return string.Format("Chunk X:{0} Z:{1}", xPosition, zPosition);
        }
    }
}
